﻿/* Copyright (C) 2007-2010 STS Soft

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; version 2 of the License.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using System.Linq.Expressions;
using System.Globalization;

namespace STSdb.General.Reflection
{
    public class NestedDefaultPropertyReflector
    {
        private Func<object, object, object> getDelegate;
        private Action<object, object, object> setDelegate;

        public Type OwnerType { get; private set; }
        public string NestedPropertyName { get; private set; }
        public Type PropertyType { get; private set; }

        public NestedDefaultPropertyReflector(Type ownerType, string nestedPropertyName)
            : this(ownerType, nestedPropertyName, null, null)
        {
        }

        public NestedDefaultPropertyReflector(Type ownerType, string nestedPropertyName, MethodInfo getConvertMethod, MethodInfo setConvertMethod)
        {
            if (nestedPropertyName == null)
                throw new ArgumentNullException("nestedPropertyName");
            if (null == ownerType)
                throw new ArgumentNullException("ownerType");

            getDelegate = MakeGet(ownerType, nestedPropertyName, getConvertMethod);
            setDelegate = MakeSet(ownerType, nestedPropertyName, setConvertMethod);

            OwnerType = ownerType;
            NestedPropertyName = nestedPropertyName;
        }

        private Func<object, object, object> MakeGet(Type ownerType, string nestedPropertyName, MethodInfo getConvertMethod)
        {
            Type nestedType = ownerType;
            var objParam = Expression.Parameter(typeof(object), "obj");
            Expression obj = Expression.Convert(objParam, ownerType);
            PropertyInfo propertyInfo;
            MethodInfo methodInfo;
            string property;
            string[] properties = nestedPropertyName.Split('.');
            for (int i = 0; i < properties.Length - 1; i++)
            {
                property = properties[i];
                propertyInfo = nestedType.GetProperty(property);
                if (propertyInfo == null)
                    throw new ArgumentException(string.Format("Property {0} {1} does not exists ", nestedPropertyName, ownerType.ToString()));

                PropertyType = propertyInfo.PropertyType;
                if (!propertyInfo.CanRead)
                    return null;

                methodInfo = propertyInfo.GetGetMethod();
                if (methodInfo == null)
                    return null;

                obj = Expression.MakeMemberAccess(obj, propertyInfo);
                nestedType = methodInfo.ReturnType;
            }

            property = properties[properties.Length - 1];
            if (property != string.Empty)
                propertyInfo = nestedType.GetProperty(property);
            else
            {
                object[] attributes = ownerType.GetCustomAttributes(typeof(DefaultMemberAttribute), true);
                if (attributes.Length == 0)
                    throw new ArgumentException(string.Format("Property {0} {1} does not exists ", nestedPropertyName, ownerType.ToString()));
                propertyInfo = nestedType.GetProperty(((DefaultMemberAttribute)attributes[0]).MemberName);
            }
            if (propertyInfo == null)
                throw new ArgumentException(string.Format("Property {0} {1} does not exists ", nestedPropertyName, ownerType.ToString()));

            PropertyType = propertyInfo.PropertyType;
            if (!propertyInfo.CanRead)
                return null;

            methodInfo = propertyInfo.GetGetMethod();
            if (methodInfo == null)
                return null;

            var indexParam = Expression.Parameter(typeof(object), "index");
            ParameterInfo[] parameterInfos = propertyInfo.GetIndexParameters();
            if (parameterInfos.Length != 1)
                throw new ArgumentException("Incorect prameter count");
            var convIndexParam = Expression.Convert(indexParam, parameterInfos[0].ParameterType);
            obj = Expression.Call(obj, methodInfo, convIndexParam);
            var objConvert = Expression.Convert(obj, typeof(object), getConvertMethod);
            var lambdaGet = Expression.Lambda<Func<object, object, object>>(objConvert, objParam, indexParam);

            return lambdaGet.Compile();
        }

        private Action<object, object, object> MakeSet(Type ownerType, string nestedPropertyName, MethodInfo setConvertMethod)
        {
            Type nestedType = ownerType;
            var param1 = Expression.Parameter(typeof(object), "obj");
            Expression obj = Expression.Convert(param1, ownerType);
            PropertyInfo propertyInfo;
            MethodInfo methodInfo;
            string property;
            string[] properties = nestedPropertyName.Split('.');
            for (int i = 0; i < properties.Length - 1; i++)
            {
                property = properties[i];
                propertyInfo = nestedType.GetProperty(property);
                if (propertyInfo == null)
                    throw new ArgumentException(string.Format("Property {0} {1} does not exists ", nestedPropertyName, ownerType.ToString()));

                PropertyType = propertyInfo.PropertyType;
                if (!propertyInfo.CanRead)
                    return null;

                methodInfo = propertyInfo.GetGetMethod();
                if (methodInfo == null)
                    return null;

                obj = Expression.MakeMemberAccess(obj, propertyInfo);
                nestedType = methodInfo.ReturnType;
            }

            property = properties[properties.Length - 1];
            if (property != string.Empty)
                propertyInfo = nestedType.GetProperty(property);
            else
            {
                object[] attributes = ownerType.GetCustomAttributes(typeof(DefaultMemberAttribute), true);
                if (attributes.Length == 0)
                    throw new ArgumentException(string.Format("Property {0} {1} does not exists ", nestedPropertyName, ownerType.ToString()));
                propertyInfo = nestedType.GetProperty(((DefaultMemberAttribute)attributes[0]).MemberName);
            }
            if (propertyInfo == null)
                throw new ArgumentException(string.Format("Property {0} {1} does not exists ", nestedPropertyName, ownerType.ToString()));

            PropertyType = propertyInfo.PropertyType;
            if (!propertyInfo.CanWrite)
                return null;

            methodInfo = propertyInfo.GetSetMethod();
            if (methodInfo == null)
                return null;

            //get = Expression.MakeMemberAccess(get, propertyInfo);
            var param2 = Expression.Parameter(typeof(object), "value");
            var indexParam = Expression.Parameter(typeof(object), "index");
            ParameterInfo[] parameterInfos = propertyInfo.GetIndexParameters();
            if (parameterInfos.Length != 1)
                throw new ArgumentException("Incorect prameter count");
            var convIndexParam = Expression.Convert(indexParam, parameterInfos[0].ParameterType);
            var value = Expression.Convert(param2, propertyInfo.PropertyType, setConvertMethod);
            var set = Expression.Call(obj, methodInfo, convIndexParam, value);
            var lambdaSet = Expression.Lambda<Action<object, object, object>>(set, param1, indexParam, param2);

            return lambdaSet.Compile();
        }

        public object GetValue(object data, object index)
        {
            return getDelegate(data, index);
        }

        public T GetValue<T>(object data, object index)
        {
            return (T)Convert.ChangeType(GetValue(data, index), typeof(T), CultureInfo.CurrentCulture);
        }

        public void SetValue(object data, object index, object value)
        {
            setDelegate(data, index, value);
        }

        public override string ToString()
        {
#if !WindowsCE && !PocketPC
            PropertyInfo pi = OwnerType.GetProperty(NestedPropertyName);
            object[] attr = pi.GetCustomAttributes(typeof(DisplayNameAttribute), false);
            if (attr.Length > 0)
                return ((DisplayNameAttribute)attr[0]).DisplayName;
#endif

            return NestedPropertyName;
        }
    }
}
